<template>
  <div
    class="rich-text-inline-editor"
    :class="{ 'is-focus': !disabled && isFocus }"
    @focus="isFocus = 1"
    @blur="isFocus = 0">
    <div
      class="rich-text-content"
      :class="contentClass"
      v-html-xss="content"
      :contenteditable="!disabled"
      ref="richContent"
      :placeholder="placeholder"
      :style="{ fontSize: fontSize + 'px' }"
      @paste.prevent="onPaste"
      @focus="onFocus"
      @blur="onBlur"
      @keyup="onKeyup"
      @mousedown="onMousedown"
      @mouseup="onMouseup"></div>
    <rich-text-tool-bar v-if="isFocus" :toolbars="toolbars"></rich-text-tool-bar>
    <rich-edit-dialog
      v-if="isFocus"
      :visible.sync="editorVisible"
      :content.sync="content"
      @updated="onRichDialogUpdated"
      @closed="resetFocus"></rich-edit-dialog>
  </div>
</template>

<script>
import { debounce } from '@utility/utility'
import RichTextToolBar from './toolBar/RichTextToolBar.vue'
import RichEditDialog from './RichEditDialog.vue'
export default {
  name: 'RichTextInline',
  components: {
    RichTextToolBar,
    RichEditDialog,
  },
  props: {
    value: {
      type: String,
      default: '',
    },
    disabled: {
      type: Boolean,
      default: false,
    },
    fontSize: {
      // 初始化展示字体大小
      type: Number,
      default: 16,
    },
    autoFocus: {
      type: Boolean,
      default: false,
    },
    initSelectAll: {
      type: Boolean,
      default: false,
    },
    contentClass: {
      type: String,
    },
    placeholder: {
      type: String,
    },
    notEmpty: {
      // 输入值后，是否可以为空
      type: Boolean,
      default: false,
    },
    toolbars: {
      type: String,
    },
    wrapFontSize: {
      type: Boolean,
      default: false,
    },
  },
  provide() {
    return {
      getSelectStyleObj: () => {
        return this.selectStyleObj
      },
      showRichEditorDialog: () => {
        this.updateValue(this.$refs.richContent.innerHTML)
        this.preventBlur = true
        this.editorVisible = true
      },
      execCommand: this.execCommand,
      resetSelection: this.resetSelection,
    }
  },
  data() {
    return {
      content: '',
      isFocus: 0,
      selectStyleObj: {
        fontSize: '',
        color: '',
        fontWeight: '',
      },
      editorVisible: false,
      preventBlur: false,
      rangeInfo: null,
      inited: false,
      oldVal: '',
      focusTimer: null,
    }
  },
  watch: {
    content(val, oldVal) {
      if (this.notEmpty && oldVal && (!val || !this.$refs.richContent.innerText.trim())) {
        this.$nextTick(() => {
          this.updateValue(oldVal)
        })
      }
    },
  },
  mounted() {
    this.setDefaultStyle()
    this.getRangeStyle = debounce(() => {
      const style = this.getSelectionStyle()
      this.selectStyleObj = style
    }, 200)

    if (this.autoFocus) {
      this.$refs.richContent.focus()
    }
  },
  methods: {
    // 对外公开，主动选中
    setFocus() {
      if (!this.isFocus) {
        this.$refs.richContent.focus()
        this.resetFocus()
      }
    },
    setDefaultStyle() {
      let content = this.value
      // 默认16px 减少默认时的内容大小
      if (this.wrapFontSize) {
        content = `<span style='font-size:${this.fontSize}px'>${this.value}</span>`
      }
      this.updateValue(content)
    },
    // 粘贴时移除html样式
    onPaste(e) {
      const clip = (e.originalEvent || e).clipboardData
      const text = clip.getData('text/plain') || ''
      if (text !== '') {
        this.execCommand('insertText', text)
      }
    },
    onBlur(e) {
      if (this.preventBlur) {
        return
      }
      if (this.isFocus === 0) {
        this.clearFocusTimer()
        this.rangeInfo = null
        return
      }
      this.isFocus = 0
      let newVal = ''
      if (e.target.innerText.trim()) {
        newVal = e.target.innerHTML
      }
      this.updateValue(newVal)
      if (this.oldVal !== newVal) {
        this.$emit('change', newVal)
      }
      this.rangeInfo = null
      this.$emit('editing', false)
    },
    onFocus() {
      this.oldVal = this.value
      // 延迟focus，解决批量添加 blur事件触发问题
      this.focusTimer = setTimeout(() => {
        this.isFocus = 1
        this.$emit('editing', true)
        if (this.initSelectAll && !this.inited) {
          this.inited = true
          this.execCommand('selectAll')
        }
      }, 100)
    },
    onKeyup() {
      this.getRangeStyle()
    },
    onMousedown() {
      this.getRangeStyle()
    },
    onMouseup() {
      const selection = window.getSelection()
      const rangeInfo = selection && selection.getRangeAt(0)
      this.rangeInfo = rangeInfo
    },
    onRichDialogUpdated() {
      // 触发对外更新
      this.updateValue(this.content)
    },
    resetFocus() {
      this.preventBlur = false
      const selection = window.getSelection()
      selection.removeAllRanges()
      const range = document.createRange()
      range && range.setStart(this.$refs.richContent, 1)
      selection.addRange(range)
      this.getRangeStyle()
    },
    updateValue(val) {
      this.content = val
      this.$emit('input', val)
    },
    clearFocusTimer() {
      this.focusTimer && clearTimeout(this.focusTimer)
    },
    getSelectionStyle() {
      const selection = window.getSelection()
      if (!selection || selection.rangeCount === 0) return
      const selectionRange = selection.getRangeAt(0)
      let startEl = selectionRange.startContainer.parentElement || selectionRange.commonAncestorContainer
      const elArr = ['P', 'DIV']
      const tagArr = ['U', 'I', 'B', 'STRIKE', 'STRONG']
      const matchedTag = []

      if (startEl.nodeType === 3) {
        startEl = startEl.parentNode
      }
      let currNode = startEl
      while (!elArr.includes(currNode.nodeName)) {
        if (!tagArr.includes(currNode.nodeName)) {
          matchedTag.push(currNode.nodeName)
        }
        currNode = currNode.parentNode
      }
      const styles = {
        italic: matchedTag.includes('I'),
        underline: matchedTag.includes('U'),
        bold: matchedTag.includes('B') || matchedTag.includes('STRONG'),
        strike: matchedTag.includes('STRIKE'),
        fontSize: null,
        color: null,
        fontWeight: null,
        fontStyle: null,
        textDecoration: null,
      }

      const currStyle = startEl.currentStyle || window.getComputedStyle(startEl, null)
      styles.bold = styles.bold || currStyle.fontWeight >= 600
      styles.italic = styles.italic || currStyle.fontStyle.includes('italic')
      styles.underline = styles.underline || currStyle.textDecorationLine === 'underline'
      styles.strike = styles.strike || currStyle.textDecorationLine === 'line-through'
      styles.fontSize = currStyle.fontSize
      styles.color = currStyle.color
      styles.fontWeight = currStyle.fontWeight
      styles.fontStyle = currStyle.fontStyle
      styles.textDecoration = currStyle.textDecoration
      return styles
    },
    execCommand(cmd, params) {
      // 解决意外失去selection情况
      this.resetSelection()
      document.execCommand('styleWithCSS', false, true)
      document.execCommand(cmd, false, params)
      document.execCommand('styleWithCSS', false, false)
      if (cmd === 'fontSize') {
        // 循环遍历 xxx-large
        const rootNode = this.rangeInfo.startContainer.parentElement || this.rangeInfo.commonAncestorContainer
        this.resetFontSize(rootNode, params)
      }
      // 有结构变化后，需更新 this.rangeInfo
      const selection = window.getSelection()
      this.rangeInfo = selection && selection.getRangeAt(0)
    },
    resetSelection() {
      const selection = window.getSelection()
      if (!selection.anchorNode && this.rangeInfo) {
        const range = document.createRange()
        range.setStart(this.rangeInfo.startContainer, this.rangeInfo.startOffset)
        range.setEnd(this.rangeInfo.endContainer, this.rangeInfo.endOffset)
        selection.addRange(range)
      }
    },
    resetFontSize(rootNode, fontsize) {
      if (rootNode.style) {
        if (['-webkit-xxx-large', 'xxx-large'].includes(rootNode.style.fontSize)) {
          rootNode.style.fontSize = fontsize
        }
        if (rootNode.hasChildNodes()) {
          for (const child of rootNode.childNodes) {
            this.resetFontSize(child, fontsize)
          }
        }
      }
    },
  },
}
</script>

<style lang="scss" scoped>
.rich-text-inline-editor {
  position: relative;
  user-select: none;
  &.is-focus {
    background: #f4f4f4;
  }
  .rich-text-content {
    outline: none;
    font-size: 16px;
    word-break: break-word;
    white-space: pre-wrap;
    &:empty::before {
      color: #aaa;
      content: attr(placeholder);
    }
  }
}
</style>
